package br.unb.core.synchronism;

import java.io.IOException;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;

import br.unb.core.dispatcher.Dispatcher;
import br.unb.utils.Console;
import br.unb.utils.Resource;

import cgl.narada.sync.SyncInfo;
import cgl.narada.sync.SyncConnection;
import cgl.narada.sync.SyncTimestamp;

public class ClientController extends Thread {

    public static int NR_AMOSTRAS_MAX = 10;
    private Synchronism syncModel;
    private SyncConnection ntpConnection;
    private SyncInfo ntpInfo;
    private byte pollInterval;
    private long waitTime;
    //private long offSetRef;
    private int precision;
    private boolean isRunning;

    /**
     * Synchronizes two machines
     *  Suppose "d" is the maximum value in milliseconds of error of the local clock with the remote clock.
     *  Be "n" the number of samples required to calculate satisfactorily the average "m" which should be compared with "d" to judge the synchronism.
     *  For each calculation of m, set the local clock with the value of m.
     *  We conclude that the two hosts are synchronized when we get the "k" number of successes (in sequence) provided m <d.
     *
     */
    public ClientController(Synchronism syncModel) throws SocketException, UnknownHostException {
        this.setName("ClientController");
        this.isRunning = false;
        this.syncModel = syncModel;
        this.precision = syncModel.getPrecision();
        this.ntpConnection = new SyncConnection(InetAddress.getByName(syncModel.getServerIp()), syncModel.getPort());
        this.startSamples();
    }

    public void run() {
        SyncTimestamp.setInitOffset(0);
        SyncTimestamp.setRefTime(0);

        this.setPriority(Thread.MAX_PRIORITY);
        long os = 0;
        int nrErros;
        long sum = 0;
        //this.sync();
        isRunning = true;
        pollInterval = ((byte) 0); //1 sample for each 2 seconds
        nrErros = 0;

        sum = 0;
        //number of samples to calculate the slope of the Offset vs Time
        int countOS = 0;
        long xSlope[] = new long[precision];
        long ySlope[] = new long[precision];
        for (int i = 0; i < precision && nrErros < NR_AMOSTRAS_MAX; i++) {
            try {
                os = getNtpSample(pollInterval);

                ySlope[i] = os;
                nrErros = 0;
                xSlope[i] = SyncTimestamp.clock.getTimeMicroseconds();
                try {
                    Thread.sleep(waitTime / 2); //1 sample for each 0.5 seconds
                } catch (InterruptedException e) {
                }

            } catch (java.net.SocketTimeoutException ste) {
                Console.printError(
                        Resource.getString("SYNCGEN") + Resource.getString("TIMEOUT"));
                nrErros++;
            } catch (IOException ex) {
                Console.printError(ex.getMessage());
                nrErros++;
            }
        }
        SyncTimestamp.setSlope(slope(xSlope, ySlope));
        SyncTimestamp.setInitOffset(initPoint(xSlope, ySlope));
        SyncTimestamp.setRefTime(xSlope[0]);
        //Notifing synchronism manager
        syncModel.setOffset((int) os);//send a message with the offset value
        try {
            os = getNtpSample(pollInterval);
        } catch (IOException ioe) {
        }
        try {
            if (nrErros < NR_AMOSTRAS_MAX) {
                Dispatcher.getInstance().sendSyncOk(syncModel);
            } else {
                Dispatcher.getInstance().sendSyncFail(syncModel);
            }
        } catch (UnknownHostException e1) {
            Console.printError(
                    Resource.getString("MANAGER_NOT_FOUND"));
        }
        Console.println(
                Resource.getString("SYNC_ESTABLISHED"));

        // Repeating calculations for a greater interval
        pollInterval = ((byte) 4); //1 sample each 16 seconds
        sum = 0;
        while (isRunning && nrErros < NR_AMOSTRAS_MAX) {
            try {
                Thread.sleep(waitTime);
            } catch (InterruptedException e) {
            }
            try {
                os = getNtpSample(pollInterval);
                if (os / 1000 > -0.5 && os / 1000 < 0.5) {
                    SyncTimestamp.addInitOffset(os);
                }
                nrErros = 0;
            } catch (java.net.SocketTimeoutException ste) {
                Console.printError(
                        Resource.getString("TIMEOUT"));
                nrErros++;

            } catch (IOException ex) {
                Console.printError(ex.getMessage());
                nrErros++;
            }
        }
        if (nrErros < NR_AMOSTRAS_MAX) {
            Console.printError(
                    Resource.getString("NUM_ERRORS_TOO_HIGH_VERIFY_SERVER_ACCESS_NEXT_TIME"));
            this.stopSamples();
        }

        Console.println(
                Resource.getString("END_OF_SYNC"));
    }

    public void startSamples() {
        if (!isRunning) {
            isRunning = true;
            this.start();
        }
    }

    public void stopSamples() {
        isRunning = false;
        this.interrupt();
    }

    public Synchronism getSyncModel() {
        return syncModel;
    }

    /**
     * Closes connection to client
     */
    public void closeConnection() {

        if (ntpConnection != null) {
            ntpConnection.close();
        }
        stopSamples();
    }

    /**
     * Gets samples from Ntp Servers. This method exahanges SyncDatagramPacket with
     * Ntp servers and sets SyncInfo object from the received NtpDatagramPackets.
     * Then SyncInfo objects are put into a queue to be processed by computeOffset()
     * to compute the offset.
     * @param interval intervalo em segundos entre duas amostras
     * @return offSet - valor a ser ajustado no relógio local
     */
    public long getNtpSample(byte interval) throws IOException {

        this.pollInterval = interval;
        long offSet = 0;
        ntpInfo = new SyncInfo();

        //Sample request
        ntpInfo = ntpConnection.getInfo();
        offSet = ntpInfo.getOffset();
        return offSet;
    }

    //TODO review method below doesnt seem to be used!!
    private void sync() {
        /*
         * n = NR_SAMPLE_MAX = 10
         * m = average
         * d = 2ms (min error ) k = 7 satisfactory successive averages
         *
         */
        final int d = 1, k = 7;
        long os = 0, soma = 0, media = 0, kCont = 0;
        int nrErros;

        try {//Grab the first sample and aproximates the local clock of the server's clock
            os = getNtpSample(pollInterval);
            SyncTimestamp.addInitOffset(os);
        } catch (IOException e) {
            e.printStackTrace();
        }
        Console.println("Synced!");
        while (kCont < k && isRunning) {
            nrErros = 0;
            for (int i = 0; i < NR_AMOSTRAS_MAX && isRunning; i++) {

                try {
                    os = getNtpSample(pollInterval);
                    //  SyncTimestamp.setOffset(os); //I'm doing this with the average of the os
                    soma += os;

                    try {
                        Thread.sleep(waitTime / NR_AMOSTRAS_MAX);
                    } catch (InterruptedException e) {
                    }

                } catch (java.net.SocketTimeoutException ste) {
                    Console.printError(
                            Resource.getString("TIMEOUT"));
                    nrErros++;
                } catch (IOException ex) {
                    Console.printError(ex.getMessage());
                    nrErros++;
                }
            }
            if (nrErros < NR_AMOSTRAS_MAX) {
                media = soma / NR_AMOSTRAS_MAX;
                SyncTimestamp.addInitOffset(media);
                if (media / 1000 < d && media / 1000 > -d) {
                    kCont++;
                } else {
                    kCont = 0;
                }
                soma = 0;
                Console.println(
                        "OffSet = " + media + "us/OffSetRef = " + SyncTimestamp.getOffset() + "us/OffSetRef2 = " + SyncTimestamp.getInitOffset());
            } else {
                Console.printError(
                        Resource.getString("VERIFY_SERVER_ACCESS"));
                this.stopSamples();
                break;
            }
            Console.println(
                    "OffSet setted to the desired value! OffSet = " + os + "ms/OffSetRef = " + SyncTimestamp.getOffset());
            isRunning = false;
        }
    }

    /**
     * Calculates the point of the line, see the class SyncTimestamp
     *
     * Y = m * x + a
     *
     * Where y indicates the offset of the local clock with respect to the remote clock
     *
     * @param x vector with samples of local times
     * @param y vector with offsets to be added to the local clock
     * So that it approaches the remote clock
     * @return a = slope of that indicates the varação offset with respect to
     * Time
     */
    public static double initPoint(long[] x, long[] y) {
        double somaX = 0, somaY = 0, somaXY = 0, somaXX = 0, somaYY = 0;
        long ref = x[0];
        for (int i = 0; i < x.length; i++) {
            somaX += (x[i] - ref);
            somaY += y[i];
            somaXX += (x[i] - ref) * (x[i] - ref);
            somaXY += (x[i] - ref) * y[i];

        }

        return (double) ((somaXX * somaY - somaX * somaXY) / (x.length * somaXX - somaX * somaX));
    }

    /**
     * Calculates the slope which approaches the change in the
     * local clock with respect to the remote clock
     *
     * y = m*x + a
     *
     * @param x vector with samples of local times
     * @param y vector of offsets to be added in the local clock
     * So that it approaches the remote clock
     * @return m slope of that indicates the variation of offset with
     * respect to time
     */
    public static double slope(long[] x, long[] y) {
        double somaX = 0, somaY = 0, somaXY = 0, somaXX = 0;
        long ref = x[0];
        //SyncTimestamp.setRefTime(ref);
        for (int i = 0; i < x.length; i++) {
            somaX += (x[i] - ref);
            somaY += y[i];
            somaXX += (x[i] - ref) * (x[i] - ref);
            somaXY += (x[i] - ref) * y[i];
        }

        return (x.length * somaXY - somaX * somaY) / (x.length * somaXX - somaX * somaX);
    }
    /*************************** Unused !!!! ***********************/
    /**
     *
     * @return log base 2 of the max interval between two requisitions
    /
    public byte getPollInterval() {
    return pollInterval;
    }

    //**
     * Sets the maximum interval between two Requisitions
     * @param power on base two used to determine the interval
    /
    public void setPollInterval(byte i) {
    this.pollInterval = i;
    setWaitTime(900 * (long) Math.pow(2, i));
    }

    /**
     * Sets the maximum wait time between two Requisitions
     * @param long indicates interval
    /
    private void setWaitTime(long wt) {
    this.waitTime = wt;
    //Resource.println("Tempo Max. de espera: "+wt+" (ms)!", client);
    }

    /**
     * Returns the offset that is currently being used by the tool
     * to simulate the time of the remote host
     * @return offset Reference offset
    /
    public long getOffSetRef() {
    return offSetRef;
    }

    /**
     * Sets the offset that is currently being used by the tool
     * to simulate the time of the remote host
     * @param offset Reference offset
    /
    public void setOffSetRef(long i) {
    this.offSetRef = i;
    }*/
}
